Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[class-parse] Loosely match parameter names, backwards #900

Merged
merged 1 commit into from
Nov 8, 2021

Conversation

jonpryor
Copy link
Member

@jonpryor jonpryor commented Oct 25, 2021

Context: 8ccb837
Context: dotnet/android-libraries#413
Context: https://discord.com/channels/732297728826277939/732297837953679412/902301741159182346
Context: https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.5.31/kotlin-stdlib-1.5.31.jar
Context: https://discord.com/channels/732297728826277939/732297837953679412/902554256035426315

dotnet/android-libraries#413 ran into an issue:

D:\a\1\s\generated\org.jetbrains.kotlin.kotlin-stdlib\obj\Release\net6.0-android\generated\src\Kotlin.Coroutines.AbstractCoroutineContextElement.cs(100,8):
error CS1002: ; expected

The offending line:

var this = Java.Lang.Object.GetObject<Java.Lang.Object> (native_this, JniHandleOwnership.DoNotTransfer);

(Assigning to this makes for a very weird error message.)

This was eventually tracked down to commit 8ccb837; @jpobst wrote:

previously it produced:

<parameter name="initial" type="R" jni-type="TR;" />
<parameter name="operation" type="kotlin.jvm.functions.Function2&lt;? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R&gt;" />

now it produces:

<parameter name="this" type="R" jni-type="TR;" />
<parameter name="initial" type="kotlin.jvm.functions.Function2&lt;? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R&gt;" />

The (a?) "source" of the problem is that Kotlin is "weird": it emits
a Java method with signature:

/* partial */ class AbstractCoroutineContextElement {
    public Object fold(Object initial, Function2 operation);
}

However, the local variables table declares three local variables:

  1. this of type kotlin.coroutines.CoroutineContext.Element
  2. initial of type java.lang.Object
  3. operation of type Function2

This is an instance method, so normally we would skip the first
local variable, as "normally" the first local variable of an instance
method has the same type as the declaring type.

The "weirdness" with Kotlin is that the first local parameter type
is not the same as the declaring type, it's of the implemented
interface type!

% mono class-parse.exe --dump kotlin/coroutines/AbstractCoroutineContextElement.class
…
ThisClass: Utf8("kotlin/coroutines/AbstractCoroutineContextElement")
…
    3: fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; Public
        Code(13, Unknown[LineNumberTable](6),
         LocalVariableTableAttribute(
          LocalVariableTableEntry(Name='this', Descriptor='Lkotlin/coroutines/CoroutineContext$Element;', StartPC=0, Index=0),
          LocalVariableTableEntry(Name='initial', Descriptor='Ljava/lang/Object;', StartPC=0, Index=1),
          LocalVariableTableEntry(Name='operation', Descriptor='Lkotlin/jvm/functions/Function2;', StartPC=0, Index=2)))
        Signature(<R:Ljava/lang/Object;>(TR;Lkotlin/jvm/functions/Function2<-TR;-Lkotlin/coroutines/CoroutineContext$Element;+TR;>;)TR;)
        RuntimeInvisibleParameterAnnotationsAttribute(Parameter0(), Parameter1(Annotation('Lorg/jetbrains/annotations/NotNull;', {})))
…

Here, we "expect" the this local variable to be of type
kotlin.coroutines.AbstractCoroutineContextElement, but it is
instead of type kotlin.coroutines.CoroutineContext.Element.

This "type mismatch" means that our logic to skip the first local
variable doesn't actually skip the first local variable.

But wait, Kotlin can throw differently weird stuff at us, too.
See e.g. inline and reified type parameters, which can result in
local parameter names such as $i$f$get, or see e.g.
CoroutinesRoom.execute(), in which the local variable table
lacks a name for one of the JNI signature parameter types.

To better address these scenarios, relax and rework the logic in
MethodInfo.UpdateParametersFromLocalVariables(): instead of
requiring that we know the "start" offset between the local variable
names and the parameters (previous world order), instead:

  1. Given names from local variables table, and parameters from
    the JNI signature,

  2. For each element in parameters, going backards, from the end
    of parameters to the front,

  3. Compare the parameters element type to each item in names,
    traversing names backwards as well.

  4. When the parameter types match, set the parameters element name
    to the names element name, then remove the names element
    from names. This prevents us from reusing the local variable
    for other parameters.

We need to do this backwards so that we "skip"/"ignore" extra
parameters at the start of the local variable name table (which is
usually the this parameter).

Not requiring that a "run" of parameter types match also grants
flexibility, as when there is no local variable for a given
parameter, we won't care.

This allows to "cleanly" handle fold(): we'll look at names for
a match for the Function2 type, find operation, then look at
names to match the Object type, find initial, then finish.

Update Xamarin.Android.Tools.Bytecode-Tests.targets so that there
are more .java files built without java -parameters, so that
the "parameter name inference" logic is actually tested.
(When javac -parameters is used, the MethodParametersAttribute
blob is emitted, which contains proper parameter names.)

@jonpryor
Copy link
Member Author

Unfortunately, while "playing around" with the source of CoroutineContext: https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/coroutines/CoroutineContext.kt

I'm unable to get kotlinc 1.5.31 to generate a fold() method in which the first local variable type differs from the declaring type: https://github.com/JetBrains/kotlin/releases/tag/v1.5.31

I don't know how to create a proper test case without figuring out how to provoke kotlin to produce that construct. :-(

@jonpryor
Copy link
Member Author

The offending line:

var this = global::Java.Lang.Object.GetObject<global::Java.Lang.Object> (native_this, JniHandleOwnership.DoNotTransfer);

@jonpryor
Copy link
Member Author

jonpryor commented Oct 26, 2021

Alternate take:

diff --git a/src/Xamarin.Android.Tools.Bytecode/Methods.cs b/src/Xamarin.Android.Tools.Bytecode/Methods.cs
index 13cd56b1..983e3f88 100644
--- a/src/Xamarin.Android.Tools.Bytecode/Methods.cs
+++ b/src/Xamarin.Android.Tools.Bytecode/Methods.cs
@@ -168,7 +168,9 @@ namespace Xamarin.Android.Tools.Bytecode {
 			int namesStart  = 0;
 			if (!AccessFlags.HasFlag (MethodAccessFlags.Static) &&
 					names.Count > namesStart &&
-					names [namesStart].Descriptor == DeclaringType.FullJniName) {
+					(names [namesStart].Descriptor == DeclaringType.FullJniName ||
+					 (names.Count > (namesStart+1) && parameters.Length > 0 &&
+					  names [namesStart+1].Descriptor == parameters [0].Type.BinaryName))) {
 				namesStart++;   // skip `this` parameter
 			}
 			if (!DeclaringType.IsStatic &&

The benefit to this is that it avoids hardcoding this, and attempts to "reset" if the 2nd local parameter type matches the first parameter type and we haven't already skipped this.

@jonpryor jonpryor force-pushed the jon-skip-this-first-params branch from 693178e to 2c88c67 Compare October 26, 2021 19:47
@jonpryor
Copy link
Member Author

@jpobst had the real alternate-take:

if we took all StartPC=0 parameters, sorted them by Index, and
then applied them to our parameters in reverse order, would that
simplify the logic of removing the ones at the beginning like
$this$find?

This is the approach that was taken.

@jonpryor jonpryor changed the title [class-parse] Skip this parameter names [class-parse] Update parameter names backwards Oct 26, 2021
jonpryor added a commit to jonpryor/xamarin-android that referenced this pull request Oct 28, 2021
Context: dotnet/java-interop#900

Update `class-parse` to better parse parameter names.
jonpryor added a commit to jonpryor/xamarin-android that referenced this pull request Oct 28, 2021
Context: dotnet/java-interop#900

Update `class-parse` to better parse parameter names.
@jonpryor jonpryor force-pushed the jon-skip-this-first-params branch from 2c88c67 to 45c0763 Compare October 28, 2021 16:17
jonpryor added a commit to jonpryor/xamarin-android that referenced this pull request Oct 28, 2021
Context: dotnet/java-interop#900

Update `class-parse` to better parse parameter names.
@jpobst
Copy link
Contributor

jpobst commented Oct 28, 2021

Running against AndroidX/GPS, there's a few differences that seem like regressions.

Xamarin.AndroidX.AppCompat.dll

Type Changed: AndroidX.AppCompat.Widget.MenuPopupWindow.MenuDropDownListView

Modified methods:

-public override bool OnTouchEvent (Android.Views.MotionEvent this_)
+public override bool OnTouchEvent (Android.Views.MotionEvent p0)

https://maven.google.com/web/index.html#androidx.appcompat:appcompat:1.3.1

I suspect this one is fine, the previous name was obviously incorrect. This may be a case where we cannot get the parameter name unless we fish it out from the base method, but I'm not certain.

Java decompiler reports it as:

public boolean onTouchEvent(MotionEvent ev)

Xamarin.AndroidX.Palette.Palette.Ktx.dll

Type Changed: AndroidX.Palette.Graphics.PaletteKt

Modified methods:

-public Palette.Swatch Get (Palette _receiver, Target target)
+public Palette.Swatch Get (Palette p0, Target p1)

https://maven.google.com/web/index.html?q=palette#androidx.palette:palette-ktx:1.0.0

This one seems like a regression, though it also looks like a Kotlin library, so it may be some weird interaction with the Kotlin fixup code.

Java decompiler reports it as:

public static final Palette.Swatch get(@NotNull Palette $receiver, @NotNull Target target) { ... }

Xamarin.Google.Android.Play.Core.dll

Type Changed: Xamarin.Google.Android.Play.Core.Review.Testing.FakeReviewManager

Modified methods:

-public virtual Xamarin.Google.Android.Play.Core.Tasks.Task LaunchReviewFlow (Android.App.Activity reviewInfo, Xamarin.Google.Android.Play.Core.Review.ReviewInfo p1)
+public virtual Xamarin.Google.Android.Play.Core.Tasks.Task LaunchReviewFlow (Android.App.Activity p0, Xamarin.Google.Android.Play.Core.Review.ReviewInfo reviewInfo)

https://maven.google.com/web/index.html?q=play#com.google.android.play:core:1.10.2

Another instance where it looks like our existing was incorrect, and our change is just different incorrect.

Java decompiler reports it as:

public Task<Void> launchReviewFlow(@NonNull Activity paramActivity, @NonNull ReviewInfo reviewInfo) { ... }

There are more reported differences, but I suspect they are duplicates of these cases.

AndroidX CI: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=5384150&view=results
GPS CI: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=5384148&view=results

Note only the Mac CI contains the changes we were testing.

@jonpryor
Copy link
Member Author

Reviewing:

Xamarin.AndroidX.AppCompat.dll

Type Changed: AndroidX.AppCompat.Widget.MenuPopupWindow.MenuDropDownListView

Modified methods:

-public override bool OnTouchEvent (Android.Views.MotionEvent this_)
+public override bool OnTouchEvent (Android.Views.MotionEvent p0)

I believe that this change is correct, if less than ideal:

% mono class-parse.exe --dump androidx/appcompat/widget/MenuPopupWindow\$MenuDropDownListView.class
…
        8: onTouchEvent (Landroid/view/MotionEvent;)Z Public, Bridge, Synthetic
                Code(6, Unknown[LineNumberTable](6), LocalVariableTableAttribute(LocalVariableTableEntry(Name='this', Descriptor='Landroidx/appcompat/widget/MenuPopupWindow$MenuDropDownListView;', StartPC=0, Index=0)))

The local variable table only declares this. There are no local variables declared of type Landroid/view/MotionEvent;.

That said, the onTouch method is "weird", in that it's Public, Bridge, Synthetic. Why is it a synthetic method? "Normally" when I see a Synthetic method, there's an "overload" of the same method with different type parameters (usually because of type erasure). I don't see that here. Weird.

@jonpryor
Copy link
Member Author

Reviewing

Xamarin.AndroidX.Palette.Palette.Ktx.dll

Type Changed: AndroidX.Palette.Graphics.PaletteKt

Modified methods:

-public Palette.Swatch Get (Palette _receiver, Target target)
+public Palette.Swatch Get (Palette p0, Target p1)
% mono class-parse.exe --dump androidx/palette/graphics/PaletteKt.class
…
        0: get (Landroidx/palette/graphics/Palette;Landroidx/palette/graphics/Target;)Landroidx/palette/graphics/Palette$Swatch; Public, Static, Final
                Code(18, LocalVariableTableAttribute(LocalVariableTableEntry(Name='$receiver', Descriptor='Landroidx/palette/graphics/Palette;', StartPC=0, Index=0), LocalVariableTableEntry(Name='target', Descriptor='Landroidx/palette/graphics/Target;', StartPC=0, Index=1), LocalVariableTableEntry(Name='$i$f$get', Descriptor='I', StartPC=0, Index=2)), Unknown[LineNumberTable](6))

As per the local variable table with StartPC=0, we have:

  • `Landroidx/palette/graphics/Palette;` $receiver
  • `Landroidx/palette/graphics/Target;` target
  • `I` $i$f$get

Which is somewhat bonkers; an int $i$f$get parameter is declared with StartPC=0, but there is no I parameter in the method signature.

The presence of the $i$f$get parameter is what's screwing things up: since we're trying to go from the "back" toward the "front", the last parameter needs to match, and it doesn't; the JNI signature has no I parameter.

I'd consider this a "Kotlin-ism", if only because I haven't seen this before. 😁

I'll try to ponder how to address this.

@jpobst
Copy link
Contributor

jpobst commented Oct 29, 2021

The Kotlin one seems to be related to "inline functions":
https://medium.com/swlh/inline-and-reified-type-parameters-in-kotlin-c7585490e103

It seems like it's always named $i$f$<method name>.

@jonpryor
Copy link
Member Author

jonpryor commented Oct 29, 2021

Indeed, and here's the source code: https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/palette/palette-ktx/src/main/java/androidx/palette/graphics/Palette.kt

The relevant method:

public inline operator fun Palette.get(target: Target): Palette.Swatch? = getSwatchForTarget(target)

Now, here's where I get confused, perhaps because this doesn't mean anything. If I try to "repro" the above, for unit test purposes:

class E (value: String) {
}

public inline operator fun E.get(c : Any?): String? = null

If I build the above with KotlinC 1.5.31-release-548, I do see a $i$f$get parameter, but it's first, not last, and it doesn't have StartPC=0, so it won't be involved in parameter name inference anyway:

	0: get (LE;Ljava/lang/Object;)Ljava/lang/String; Public, Static, Final
		Code(10, Unknown[LineNumberTable](6), 
		 LocalVariableTableAttribute(
		  LocalVariableTableEntry(Name='$i$f$get', Descriptor='I', StartPC=8, Index=2),
		  LocalVariableTableEntry(Name='$this$get', Descriptor='LE;', StartPC=0, Index=0),
		  LocalVariableTableEntry(Name='c', Descriptor='Ljava/lang/Object;', StartPC=0, Index=1)))

The question is, does this matter? Would this be a "good enough" unit test?

@jonpryor jonpryor force-pushed the jon-skip-this-first-params branch 2 times, most recently from 0213c4a to 083e2ac Compare October 29, 2021 20:29
jonpryor added a commit to jonpryor/xamarin-android that referenced this pull request Oct 29, 2021
Context: dotnet/java-interop#900

Update `class-parse` to better parse parameter names.
@jonpryor
Copy link
Member Author

@jpobst: updated with yet another idea for fixing the scenario, which appears to better handle PaletteKt.

I've also kicked off a rebuild of dotnet/android#6442

@jpobst
Copy link
Contributor

jpobst commented Nov 1, 2021

This seems to have created a different issue which is hitting quite a few assemblies, though it looks like it could be just one issue:

API diff: Xamarin.AndroidX.Leanback.dll

Xamarin.AndroidX.Leanback.dll

Namespace AndroidX.Leanback.Graphics

Type Changed: AndroidX.Leanback.Graphics.BoundsRule

Modified constructors:

-public BoundsRule (BoundsRule boundsRule)
+public BoundsRule (BoundsRule this_)

Namespace AndroidX.Leanback.Widget

Type Changed: AndroidX.Leanback.Widget.BaseCardView

Type Changed: AndroidX.Leanback.Widget.BaseCardView.LayoutParams

Modified constructors:

-public BaseCardView.LayoutParams (BaseCardView.LayoutParams source)
+public BaseCardView.LayoutParams (BaseCardView.LayoutParams this_)

Full results:
https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=5393894&view=artifacts&pathAsName=false&type=publishedArtifacts

Click nuget -> api-diff to see the results. There will be a few unrelated changes sprinkled in, but most are parameter name changes.

@jonpryor
Copy link
Member Author

jonpryor commented Nov 1, 2021

@jpobst wrote:

…though it looks like it could be just one issue:

-public BoundsRule (BoundsRule boundsRule)
+public BoundsRule (BoundsRule this_)

It's what I "feared"; as per the current PR message:

instead of requiring that we know the "start" offset between the local variable
names and the parameters (previous world order), instead look for a
"run" of local variables which have the same types, in the same
order, as the descriptor parameters. If there are extra local
variables, ignore them.

Let's take the BoundsRule(BoundsRule) constructor as an example: the local variables table is:

LocalVariableTableAttribute(
  LocalVariableTableEntry(Name='this', Descriptor='Landroidx/leanback/graphics/BoundsRule;', StartPC=0, Index=0), 
  LocalVariableTableEntry(Name='boundsRule', Descriptor='Landroidx/leanback/graphics/BoundsRule;', StartPC=0, Index=1))

Because we no longer attempt to skip the first parameter, and we're looking for a parameter list of (BoundsRule), we match on the this parameter.

To fix this, I'll need to search from the end of the local variables table list, as I did in a previous version of this PR, while also maintaining the "match runs" logic.

@jonpryor jonpryor force-pushed the jon-skip-this-first-params branch from 083e2ac to c1e9c0a Compare November 1, 2021 18:06
jonpryor added a commit to jonpryor/xamarin-android that referenced this pull request Nov 1, 2021
Context: dotnet/java-interop#900

Update `class-parse` to better parse parameter names.
@jpobst
Copy link
Contributor

jpobst commented Nov 1, 2021

The only change that seems like a regression I see is:

API diff: Xamarin.AndroidX.Room.Room.Ktx.dll

Xamarin.AndroidX.Room.Room.Ktx.dll

Namespace AndroidX.Room

Type Changed: AndroidX.Room.CoroutinesRoom

Modified methods:

-public Java.Lang.Object Execute (RoomDatabase db, bool inTransaction, Java.Util.Concurrent.ICallable callable, Kotlin.Coroutines.IContinuation p3)
+public Java.Lang.Object Execute (RoomDatabase p0, bool p1, Java.Util.Concurrent.ICallable p2, Kotlin.Coroutines.IContinuation p3)
-public Java.Lang.Object Execute (RoomDatabase db, bool inTransaction, Android.OS.CancellationSignal cancellationSignal, Java.Util.Concurrent.ICallable callable, Kotlin.Coroutines.IContinuation p4)
+public Java.Lang.Object Execute (RoomDatabase p0, bool p1, Android.OS.CancellationSignal p2, Java.Util.Concurrent.ICallable p3, Kotlin.Coroutines.IContinuation p4)

AndroidX artifacts: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=5395258&view=artifacts&pathAsName=false&type=publishedArtifacts
GPS artifacts: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=5395259&view=artifacts&pathAsName=false&type=publishedArtifacts

@jonpryor
Copy link
Member Author

jonpryor commented Nov 1, 2021

I'm not sure how to support this scenario.

If I grab androidx.room.room-ktx-2.3.0 and dump androidx/room/CoroutinesRoom.class:

% curl -o x.zip https://dl.google.com/android/maven2/androidx/room/room-ktx/2.3.0/room-ktx-2.3.0.aar
% unzip x.zip classes.jar
% unzip classes.jar androidx/room/CoroutinesRoom.class
% mono class-parse.exe --dump androidx/room/CoroutinesRoom.class

We can see the local variable table for execute:

LocalVariableTableAttribute(
    LocalVariableTableEntry(Name='this', Descriptor='Landroidx/room/CoroutinesRoom$Companion;', StartPC=0, Index=0),
    LocalVariableTableEntry(Name='db', Descriptor='Landroidx/room/RoomDatabase;', StartPC=0, Index=1),
    LocalVariableTableEntry(Name='inTransaction', Descriptor='Z', StartPC=0, Index=2),
    LocalVariableTableEntry(Name='callable', Descriptor='Ljava/util/concurrent/Callable;', StartPC=0, Index=3)))

Thus, the local variable table implies parameters of:

  • androidx.room.CoroutinesRoom.Companion this
  • androidx.room.RoomDatabase db
  • boolean inTransaction
  • java.util.concurrent.Callable callable

Compare to the JNI signature of (Landroidx/room/RoomDatabase;ZLjava/util/concurrent/Callable;Lkotlin/coroutines/Continuation;):

  • androidx.room.RoomDatabase
  • Z (boolean)
  • java.util.concurrent.Callable
  • kotlin.coroutines.Continuation

The expectation is that the local variable table will be a superset of the JNI signature types, in the "same" order.

The reality doesn't match. There's an "overlapping" order: (RoomDatabase db, boolean inTransaction, Callable callable), but then the JNI signature wants a Continuation parameter, which is not present or listed in the local variables table.

@jpobst: thus, a question:

  1. take the current PR as-is and use Metadata to update the method which changed (it's only one), or
  2. Further "relax" the match logic so that we don't require that all JNI signature types match, but only some of the m.

I'm not really sure what (2) means; what is "some"? Should it match at least one? Or just "pairwise match" based on types from the end of the parameter list & local variables table toward the front, "removing" previously matched items?

That might not be a bad idea, actually…?

Context: 8ccb837
Context: dotnet/android-libraries#413
Context: https://discord.com/channels/732297728826277939/732297837953679412/902301741159182346
Context: https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.5.31/kotlin-stdlib-1.5.31.jar
Context: https://discord.com/channels/732297728826277939/732297837953679412/902554256035426315

dotnet/android-libraries#413 ran into an issue:

	D:\a\1\s\generated\org.jetbrains.kotlin.kotlin-stdlib\obj\Release\net6.0-android\generated\src\Kotlin.Coroutines.AbstractCoroutineContextElement.cs(100,8):
	error CS1002: ; expected

The offending line:

	var this = Java.Lang.Object.GetObject<Java.Lang.Object> (native_this, JniHandleOwnership.DoNotTransfer);

(Assigning to `this` makes for a very weird error message.)

This was eventually tracked down to commit 8ccb837; @jpobst wrote:

> previously it produced:
>
>     <parameter name="initial" type="R" jni-type="TR;" />
>     <parameter name="operation" type="kotlin.jvm.functions.Function2&lt;? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R&gt;" />
>
> now it produces:
>
>     <parameter name="this" type="R" jni-type="TR;" />
>     <parameter name="initial" type="kotlin.jvm.functions.Function2&lt;? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R&gt;" />

The (a?) "source" of the problem is that Kotlin is "weird": it emits
a Java method with signature:

	/* partial */ class AbstractCoroutineContextElement {
	    public Object fold(Object initial, Function2 operation);
	}

However, the local variables table declares *three* local variables:

 1. `this` of type `kotlin.coroutines.CoroutineContext.Element`
 2. `initial` of type `java.lang.Object`
 3. `operation` of type `Function2`

This is an instance method, so normally we would skip the first
local variable, as "normally" the first local variable of an instance
method has the same type as the declaring type.

The "weirdness" with Kotlin is that the first local parameter type
is *not* the same as the declaring type, it's of the implemented
interface type!

	% mono class-parse.exe --dump kotlin/coroutines/AbstractCoroutineContextElement.class
	…
	ThisClass: Utf8("kotlin/coroutines/AbstractCoroutineContextElement")
	…
	    3: fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; Public
	        Code(13, Unknown[LineNumberTable](6),
	         LocalVariableTableAttribute(
	          LocalVariableTableEntry(Name='this', Descriptor='Lkotlin/coroutines/CoroutineContext$Element;', StartPC=0, Index=0),
	          LocalVariableTableEntry(Name='initial', Descriptor='Ljava/lang/Object;', StartPC=0, Index=1),
	          LocalVariableTableEntry(Name='operation', Descriptor='Lkotlin/jvm/functions/Function2;', StartPC=0, Index=2)))
	        Signature(<R:Ljava/lang/Object;>(TR;Lkotlin/jvm/functions/Function2<-TR;-Lkotlin/coroutines/CoroutineContext$Element;+TR;>;)TR;)
	        RuntimeInvisibleParameterAnnotationsAttribute(Parameter0(), Parameter1(Annotation('Lorg/jetbrains/annotations/NotNull;', {})))
	…

Here, we "expect" the `this` local variable to be of type
`kotlin.coroutines.AbstractCoroutineContextElement`, but it is
instead of type `kotlin.coroutines.CoroutineContext.Element`.

This "type mismatch" means that our logic to skip the first local
variable doesn't actually skip the first local variable.

But wait, Kotlin can throw differently weird stuff at us, too.
See e.g. [inline and reified type parameters][0], which can result in
local parameter names such as `$i$f$get`, or see e.g.
[`CoroutinesRoom.execute()`][1], in which the local variable table
*lacks* a name for one of the JNI signature parameter types.

To better address these scenarios, relax and rework the logic in
`MethodInfo.UpdateParametersFromLocalVariables()`: instead of
requiring that we know the "start" offset between the local variable
names and the parameters (previous world order), instead:

 1. Given `names` from local variables table, and `parameters` from
    the JNI signature,

 2. For each element in `parameters`, going *backards*, from the end
    of `parameters` to the front,

 3. Compare the `parameters` element type to each item in `names`,
    traversing `names` backwards as well.

 4. When the parameter types match, set the `parameters` element name
    to the `names` element name, then *remove* the `names` element
	from `names`.  This prevents us from reusing the local variable
	for other parameters.

We need to do this backwards so that we "skip"/"ignore" extra
parameters at the start of the local variable name table (which is
usually the `this` parameter).

*Not* requiring that a "run" of parameter types match also grants
flexibility, as when there is no local variable for a given
parameter, we won't care.

This allows to "cleanly" handle `fold()`: we'll look at `names` for
a match for the `Function2` type, find `operation`, then look at
`names` to match the `Object` type, find `initial`, then finish.

Update `Xamarin.Android.Tools.Bytecode-Tests.targets` so that there
are more `.java` files built *without* `java -parameters`, so that
the "parameter name inference" logic is actually tested.
(When `javac -parameters` is used, the `MethodParametersAttribute`
blob is emitted, which contains proper parameter names.)

[0]: https://medium.com/swlh/inline-and-reified-type-parameters-in-kotlin-c7585490e103
[1]: dotnet#900 (comment)
@jonpryor jonpryor changed the title [class-parse] Update parameter names backwards [class-parse] Loosely match parameter names, backwards Nov 2, 2021
@jonpryor jonpryor force-pushed the jon-skip-this-first-params branch from c1e9c0a to 443bd03 Compare November 2, 2021 01:53
jonpryor added a commit to jonpryor/xamarin-android that referenced this pull request Nov 2, 2021
Context: dotnet/java-interop#900

Update `class-parse` to better parse parameter names.
@jonpryor
Copy link
Member Author

jonpryor commented Nov 2, 2021

Or just "pairwise match" based on types from the end of the parameter list & local variables table toward the front, "removing" previously matched items?

So I did that, and updated dotnet/android#6442

Will It Build™?

jonpryor added a commit to jonpryor/xamarin-android that referenced this pull request Nov 3, 2021
Context: dotnet/java-interop#900

Update `class-parse` to better parse parameter names.
@jpobst
Copy link
Contributor

jpobst commented Nov 5, 2021

AndroidX looks good, some issues found with GPS:

Xamarin.Google.MLKit.BarcodeScanning.dll

Type Changed: Xamarin.Google.MLKit.Barhopper.Barcode.Address

Modified methods:

-public virtual void WriteToParcel (Android.OS.Parcel this_, Android.OS.ParcelableWriteFlags dest)
+public virtual void WriteToParcel (Android.OS.Parcel dest, Android.OS.ParcelableWriteFlags p1)

Android.OS.ParcelableWriteFlags should be named flags. I don't know if the data is actually there, but I suspect it might be, and if you're doing type matching you need to ensure that [GeneratedEnum] counts as a match for int.

Xamarin.Google.Android.Play.Core.dll

Type Changed: Xamarin.Google.Android.Play.Core.AppUpdate.Testing.FakeAppUpdateManager

Modified methods:

-public virtual bool StartUpdateFlowForResult (Xamarin.Google.Android.Play.Core.AppUpdate.AppUpdateInfo appUpdateInfo, int appUpdateType, Xamarin.Google.Android.Play.Core.Common.IIntentSenderForResultStarter p2, int p3)
+public virtual bool StartUpdateFlowForResult (Xamarin.Google.Android.Play.Core.AppUpdate.AppUpdateInfo appUpdateInfo, int p1, Xamarin.Google.Android.Play.Core.Common.IIntentSenderForResultStarter p2, int appUpdateType)

Parameter 2 should be named appUpdateType. Instead it was moved to parameter 4.

AppUpdateInfo Documentation

@jonpryor
Copy link
Member Author

jonpryor commented Nov 6, 2021

@jpobst: I can't find the .aar that contains Google.MLKit.Barhopper.Barcode.Address. Could you provide the link?

if you're doing type matching you need to ensure that [GeneratedEnum] counts as a match for int.

This doesn't quite make sense; this PR is about updating Xamarin.Android.Tools.Bytecode, which doesn't parse IL or know anything about [GeneratedEnum]. That's a generator-side construct.


I thus turn my attention to FakeAppUpdateManager, which is weird:

curl -o x.zip https://dl.google.com/android/maven2/com/google/android/play/core/1.10.2/core-1.10.2.aar
unzip x.zip classes.jar
unzip classes.jar 'com/google/android/play/core/appupdate/testing/FakeAppUpdateManager.class'

mono …/class-parse.exe --dump com/google/android/play/core/appupdate/testing/FakeAppUpdateManager.class

There are 4 startUpdateFlowForResult() methods, only two of which has a IntentSenderForResultStarter type, and of those two methods, only one has a local variable table. Reformatting for readability:

LocalVariableTableAttribute(
    LocalVariableTableEntry(Name='this', Descriptor='Lcom/google/android/play/core/appupdate/testing/FakeAppUpdateManager;', StartPC=0, Index=0),
    LocalVariableTableEntry(Name='appUpdateInfo', Descriptor='Lcom/google/android/play/core/appupdate/AppUpdateInfo;', StartPC=0, Index=1),
    LocalVariableTableEntry(Name='appUpdateType', Descriptor='I', StartPC=0, Index=2)))

There are only three local variables, of which one is this, and thus should be ignored. For a method which takes 4 parameters.

Uh… 😅

I think two of these parameters are unnamed.

Which likely explains why we're getting things "wrong". The current algorithm tries to match variables based on type, starting at the end of the parameter list. There are 2 int local variables, so we use the last one for the last parameter, hence the reported change.


I don't know if there's anything that class-parse can actually do about this situation. It was wrong before (hence the p2 and p3 args), it's wrong now, and it can't be "right" when data is missing. (Yay Metadata?)

Meanwhile, while there isn't anything class-parse can do here, we aren't restricted to class-parse. There's source code!

curl -o s.zip https://dl.google.com/android/maven2/com/google/android/play/core/1.10.2/core-1.10.2-sources.jar
unzip s.zip com/google/android/play/core/appupdate/testing/FakeAppUpdateManager.java

FakeAppUpdateManager.java contains:

public boolean startUpdateFlowForResult(com.google.android.play.core.appupdate.AppUpdateInfo appUpdateInfo, int appUpdateType, com.google.android.play.core.common.IntentSenderForResultStarter starter, int requestCode) { throw new RuntimeException("Stub!"); }

Can java-source-utils (69e1b80) deal with it?

% curl -o s.zip https://dl.google.com/android/maven2/com/google/android/play/core/1.10.2/core-1.10.2-sources.jar
% java -jar …/java-source-utils.jar s.zip
      <method jni-return="Z" jni-signature="(Lcom/google/android/play/core/appupdate/AppUpdateInfo;ILcom/google/android/play/core/common/IntentSenderForResultStarter;I)Z" name="startUpdateFlowForResult" return="boolean">
        <parameter jni-type="Lcom/google/android/play/core/appupdate/AppUpdateInfo;" name="appUpdateInfo" type="com.google.android.play.core.appupdate.AppUpdateInfo"/>
        <parameter jni-type="I" name="appUpdateType" type="int"/>
        <parameter jni-type="Lcom/google/android/play/core/common/IntentSenderForResultStarter;" name="starter" type="com.google.android.play.core.common.IntentSenderForResultStarter"/>
        <parameter jni-type="I" name="requestCode" type="int"/>
        <javadoc>

Yes!

I don't currently know where to look to see how Xamarin.Google.Android.Play.Core is bound, but if it isn't already using @(JavaSourceJar), could it be updated to do so? (Plus, java-source-utils could use more testing anyway…)

@jonpryor
Copy link
Member Author

jonpryor commented Nov 6, 2021

Regarding the use of java-source-utils to import parameter names, see also:

@jpobst
Copy link
Contributor

jpobst commented Nov 6, 2021

I can't find the .aar that contains Google.MLKit.Barhopper.Barcode.Address. Could you provide the link?

Should be here: https://maven.google.com/web/index.html?q=Barcode#com.google.mlkit:barcode-scanning:17.0.0

This doesn't quite make sense; this PR is about updating Xamarin.Android.Tools.Bytecode, which doesn't parse IL or know anything about [GeneratedEnum]. That's a generator-side construct.

Good point. I was looking at compares done on managed code and forgot how things work. 😁

@jonpryor
Copy link
Member Author

jonpryor commented Nov 6, 2021

After some back and forth, we found that we wanted com.google.mlkit.barcode-scanning version 16.1.2, not 17, in order to see the problem.

I don't see a problem with the change. Explanation:

% curl -o x.zip https://dl.google.com/android/maven2/com/google/mlkit/barcode-scanning/16.1.2/barcode-scanning-16.1.2.aar
% unzip x.zip classes.jar
% unzip classes.jar 'com/google/android/libraries/barhopper/Barcode$Address.class'
% mono …/class-parse.exe --dump 'unzip classes.jar 'com/google/android/libraries/barhopper/Barcode$Address.class'
…
        5: writeToParcel (Landroid/os/Parcel;I)V Public
                Code(17, Unknown[LineNumberTable](10), LocalVariableTableAttribute(LocalVariableTableEntry(Name='this', Descriptor='Lcom/google/android/libraries/barhopper/Barcode$Address;', StartPC=0, Index=0), LocalVariableTableEntry(Name='dest', Descriptor='Landroid/os/Parcel;', StartPC=0, Index=1)))
                RuntimeInvisibleParameterAnnotationsAttribute(Parameter0(Annotation('Landroidx/annotation/RecentlyNonNull;', {})), Parameter1())

Reformat for legibility, and the local variable table for writeToParcel() has:

LocalVariableTableAttribute(
    LocalVariableTableEntry(Name='this', Descriptor='Lcom/google/android/libraries/barhopper/Barcode$Address;', StartPC=0, Index=0),
    LocalVariableTableEntry(Name='dest', Descriptor='Landroid/os/Parcel;', StartPC=0, Index=1)))

Similar to what we saw above with FakeAppUpdateManager: we're "missing" some local variables, in particular a local variable for the I parameter.

In the PR, we match the local variable type to the descriptor type (line 3): https://github.com/xamarin/java.interop/pull/900/files#diff-ee31a70300da8a52eb97d7493490e6eecea37ba4170fad7227b43c11675c5fddR173

for (int pi = parametersCount-1; pi >= 0; --pi) {
	for (int ni = names.Count-1; ni >= 0; --ni) {
		if (parameters [pi].Type.BinaryName != names [ni].Descriptor) {
			continue;
		}
		parameters [pi].Name = names [ni].Name;
		names.RemoveAt (ni);
		break;
	}
}

meaning we try to find a match for parameter I, and don't find one; there is no local variable of type I. It's thus unchanged, and retains the name p1.

We then proceed to the next parameter (reverse order), of type android.os.Parcel, and find the dest local variable.

We skip this, as no parameter types match the local variable type of this.

Thus our XML:

% mono …/class-parse.exe 'com/google/android/libraries/barhopper/Barcode$Address.class'
…
      <method
        abstract="false"
        deprecated="not deprecated"
        final="false"
        name="writeToParcel"
        native="false"
        return="void"
        jni-return="V"
        static="false"
        synchronized="false"
        visibility="public"
        bridge="false"
        synthetic="false"
        jni-signature="(Landroid/os/Parcel;I)V">
        <parameter
          name="dest"
          type="android.os.Parcel"
          jni-type="Landroid/os/Parcel;"
          not-null="true" />
        <parameter
          name="p1"
          type="int"
          jni-type="I" />
      </method>

which matches the above report:

-public virtual void WriteToParcel (Android.OS.Parcel this_, Android.OS.ParcelableWriteFlags dest)
+public virtual void WriteToParcel (Android.OS.Parcel dest, Android.OS.ParcelableWriteFlags p1)

With the data at hand, this is the appropriate result. There's no problem here.

(Pity the barcode-scanning library doesn't contain a Source jar…)

Copy link
Contributor

@jpobst jpobst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest diffs look good.

@jonpryor jonpryor merged commit dcdcce1 into dotnet:main Nov 8, 2021
@github-actions github-actions bot locked and limited conversation to collaborators Apr 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants